/**
 * 
 */
package com.epart.crypt;

import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;


/**
 * AES128, SHA256 암호화 클래스
 * 
 * @author cwsong, bskim
 * @since 2012. 6. 7.
 */
public class EpartSample {
	
	private static final String SECRET_KEY 	= "$R-_0H;&qZF4|$o+9(S";
	private static final String IV_SEED 		= "$R-_0H;&qZF4|$o+9(S";
	private static final String SALT 			= "$R-_0H;&qZF4|$o+9(S";

	private static final String CHARSET = "UTF-8";
	private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";

	private static MessageDigest md5;
	private static SecretKeySpec secretKeySpec = null;
	private static IvParameterSpec initialVector = null;
	private static Cipher encryptCipher = null;
	private static Cipher decryptCipher = null;
	
	/**
	 * 초기화 메서드
	 */
	static {
		try {
			md5 = MessageDigest.getInstance("MD5");
			secretKeySpec = new SecretKeySpec(md5.digest(SECRET_KEY.getBytes("UTF8")), "AES");
			initialVector = new IvParameterSpec(md5.digest(IV_SEED.getBytes("UTF8")));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
	}
	
	/*
	 * 양방향 암호화 인스턴스를 반환한다.
	 * 
	 * @return
	 * @throws InvalidKeyException
	 * @throws InvalidAlgorithmParameterException
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 */
	private static Cipher getEncryptCipher() throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
		if (encryptCipher == null) {
			System.out.println(">>> EpartCrypt.getEncryptCipher >>>" + " new encryptCipher instanc" );
			encryptCipher = Cipher.getInstance(TRANSFORMATION);
			encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, initialVector);
		}
		return encryptCipher;
	}
	
	/*
	 * 양방향 복호화 인스턴스를 반환한다.
	 * 
	 * @return
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 * @throws InvalidKeyException
	 * @throws InvalidAlgorithmParameterException
	 */
	private static Cipher getDecryptCipher() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
		if (decryptCipher == null) {
			System.out.println(">>> EpartCrypt.getEncryptCipher >>>" + " new decryptCipher instanc" );
			decryptCipher = Cipher.getInstance(TRANSFORMATION);
			decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, initialVector);
		}
		return decryptCipher;
	}
	
	/**
	 * 암호화를 수행한다. 문자열을 받아 복호화 가능한 암호문자열로 변환하여 반환한다.
	 * 
	 * @param cleanStr
	 * @return 암호화 문자열
	 */
	public static String encrypt(String cleanStr) {
		byte[] bytes = null;
		try {
			bytes = getEncryptCipher().doFinal(cleanStr.getBytes(CHARSET));
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (IllegalStateException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return Base64Coder.encodeLines(bytes).trim().replaceAll("\r\n", "");
	}
	
	/**
	 * 암호화된 문자열을 복호화 하여 반환한다.
	 * 
	 * @param encryptStr
	 * @return 복호된 문자열
	 */
	public static String decrypt(String encryptStr) {
		byte[] bytes = null;
		try {
			bytes = getDecryptCipher().doFinal(Base64Coder.decodeLines(encryptStr));
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (IllegalStateException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			e.printStackTrace();
		}
		return new String(bytes).trim().replaceAll("\r\n", "");
	}
	
	/*---------------------위쪽은 블록암호(양방향)소스/아래쪽은 해쉬알고리즘(일방향 관련) 소스-------------------------*/
	
	private static MessageDigest sha256;
	
	/*
	 * 일방향 암호화 인스턴스를 반환한다.
	 * 
	 * @return messageDigest 인스턴스
	 * @throws NoSuchAlgorithmException
	 */
	private static MessageDigest getSha256() throws NoSuchAlgorithmException {
		if (sha256 == null) {
			System.out.println(">>> EpartCrypt.getEncryptCipher >>>" + " new sah256 instanc" );
			sha256 = MessageDigest.getInstance("SHA-256");
		}
		return sha256;
	}
	
	/**
	 * 복호화 할 수 없는 해쉬알고리즘 암호화를 수행한다.
	 * 비밀번호등에 사용.
	 *  
	 * @param input
	 * @return 암호화 문자열
	 */
	public static String sha256(String input) throws NoSuchAlgorithmException {
		if (input == null) {
			return null;
		}
		input = SALT + input;
		return sha256(input.getBytes());
	}
	
	/**
	 * 복호화 할 수 없는 해쉬알고리즘 암호화를 수행한다.
	 * 암호문자열이 특정 문자셋으로 구성되어 있을경우 사용할 문자셋을 지정한다. 
	 * 비밀번호등에 사용.
	 *  
	 * @param input
	 * @param charsetName
	 * @return 암호화 문자열
	 */
	public static String sha256(String input, String charsetName) throws UnsupportedEncodingException, NoSuchAlgorithmException {
		if (input == null) {
			return null;
		}
		input = SALT + input;
		return sha256(input.getBytes(charsetName));
	}
	
	/*
	 * 바이너리 배열을 받아 암호화를 수행하는 내부 메서드
	 * @param input
	 * @return 암호화 결과값
	 * @throws NoSuchAlgorithmException
	 */
	private static String sha256(byte[] input) throws NoSuchAlgorithmException {
		byte[] digest = null;
		// Stage 1
		digest = getHash(input);
		// Stage 2
		digest = getHash(digest);

		StringBuffer sb = new StringBuffer(1 + digest.length);
		sb.append("*");
		sb.append(toHexString(digest).toUpperCase());
		return sb.toString().trim().replaceAll("\r\n", "");
	}

	/*
	 * <p>
	 * unsigned byte(바이트) 배열을 16진수 문자열로 바꾼다.
	 * </p>
	 * 
	 * <pre>
	 * ByteUtils.toHexString(null)                   = null
	 * ByteUtils.toHexString([(byte)1, (byte)255])   = "01ff"
	 * </pre>
	 * 
	 * @param bytes unsigned byte's array
	 * @return hexString
	 * @see HexUtils.toString(byte[])
	 */
	private static String toHexString(byte[] bytes) {
		if (bytes == null) {
			return null;
		}

		StringBuffer result = new StringBuffer();
		for (int i = 0; i < bytes.length; i++) {
			byte b = bytes[i];
			result.append(Integer.toString((b & 0xF0) >> 4, 16));
			result.append(Integer.toString(b & 0x0F, 16));
		}
		return result.toString();
	}
	
	/*
	 * <p>입력한 데이터(바이트 배열)을 SHA1 알고리즘으로 처리하여 해쉬값을 도출한다.</p>
	 *
	 * <pre>
	 * getHash([0x68, 0x61, 0x6e]) = [0x4f, 0xf6, 0x15, 0x25, 0x34, 0x69, 0x98, 0x99, 0x32, 0x53, 0x2e, 0x92, 0x60, 0x06, 0xae, 0x5c, 0x99, 0x5e, 0x5d, 0xd6]
	 * </pre>
	 *
	 * @param input 입력 데이터(<code>null</code>이면 안된다.)
	 * @return 해쉬값
	 */
	private static byte[] getHash(byte[] input) throws NoSuchAlgorithmException {
		if( input == null ){
			throw new IllegalArgumentException("input value is NULL");
		}
		return getSha256().digest(input);
	}
}
